# Copyright (c) 2021-2024 Huawei Device Co., Ltd.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'yaml'
require 'ostruct'
require 'set'
require 'delegate'

class JSString
  def initialize(str)
    raise if !str.kind_of?(String)
    @str = str
  end

  def to_cpp_literal()
    return "\"#{@str}\""
  end

  def to_cpp_value()
    if is_global_constant?
      return "thread_->GlobalConstants()->GetHandled#{@str[0].upcase + @str[1..-1]}String()"
    else 
      return "JSHandle<JSTaggedValue>(factory_->NewFromCanBeCompressString(\"#{@str}\"))"
    end
  end

  def is_global_constant?
    case @str
    when  "toLocaleString",
          "toString",
          "valueOf",
          "set",
          "get",
          "baseName",
          "calendar",
          "caseFirst",
          "collation",
          "hourCycle",
          "numberingSystem",
          "numeric",
          "language",
          "script",
          "region",
          "format"
      return true
    else
      return false
    end
  end

  def ==(other)
    other.kind_of?(JSString) && other.raw == @str
  end

  def raw() @str end
end

class JSSymbol
  def initialize(str)
    raise if !str.kind_of?(String)
    @str = str
  end

  def to_cpp_literal(try_resolve_global_const = false)
    return "\"[Symbol.#{@str}]\""
  end

  def to_cpp_value()
    return "env->Get#{@str[0].upcase + @str[1..-1]}Symbol()"
  end

  def raw() @str end
end

class CPPImpl
  def initialize(str)
    if str.kind_of?(Numeric)
      str = String(str)
    end
    @str = str
    raise if !@str.kind_of?(String)
  end

  def raw() @str end
end

class Alias
  def initialize(str)
    @obj = nil
    @is_proto = nil
    @key = nil
    #TODO: parse properly
    chain = str.split(".")
    @key = JSString.new(chain[-1])
    case chain.size
    when 2
      @obj = chain[0]
      @is_proto = false
    when 3
      @obj = chain[0]
      @is_proto = true
    end
  end

  def gen_cpp_obj_getter
    res = ""
    if @obj == nil
      res += "env->GetJSGlobalObject()"
    else
      res += "env->Get#{@obj[0].upcase + @obj[1..-1]}"
      if @is_proto
        res += "Prototype()"
      else
        res += "Function()"
      end
    end
    res
  end

  def gen_cpp_prop_getter
    res = "JSObject::GetProperty(thread_, "
    res += gen_cpp_obj_getter + ", "
    res += @key.to_cpp_value + ").GetValue()"
    res
  end
end

class CtorDescr
  def initialize(ostr, is_function_ctor = false)
    raise "Not OpenStruct" if !ostr.kind_of?(OpenStruct)
    @is_function_ctor = is_function_ctor
    ostr.each_pair do |key, value|
      case key.to_s
      when "impl"
        @impl = CPPImpl.new value
      when "argc"
        @argc = value
      else
        raise "Unknown field: " + key.to_s
      end
    end
    # check created struct:
    raise "Invalid `impl` type" if !@impl.kind_of?(CPPImpl)  # cpp-impl
    raise "Invalid `argc` type" if !@argc.kind_of?(Numeric)
  end

  def gen_cpp_id
    @impl.raw
  end

  def impl() @impl end
  def argc() @argc end
end

class InlinableDescr
  def initialize(ostr, argc)
    raise "Not OpenStruct" if !ostr.kind_of?(OpenStruct)
    raise "Not Integer" if !argc.kind_of?(Integer)
    ostr.each_pair do |key, value|
      case key.to_s
      when "intrinsic"
        @intrinsic = Runtime.intrinsics.find { |i| i.name == value }
        warn "[Builtins] Warning: Intrinsic `#{value}` not found" if @intrinsic == nil
      when "instruction"
        @instruction = IR.instructions.find { |i| i.opcode == value }
        warn "[Builtins] Warning: Instruction `#{value}` not found" if @instruction == nil
      when "args_type"
        raise "`args_type` should be an array" if !value.kind_of?(Array)
        raise "`args_type` size should be equal to argc" if value.size != argc
        @args_type = value
      when "res_type"
        @res_type = value
      else
        raise "Unknown field: " + key.to_s
      end
    end
    # check created struct:
    raise "Missing `args_type` field" if @args_type == nil
    raise "Missing `res_type` field" if @res_type == nil
    raise "Invalid `intrinsic` and `instruction` field" if (@intrinsic != nil) && (@instruction != nil)
  end

  def GetIRArgType(idx)
    "AnyBaseTypeToDataType(AnyBaseType::#{@args_type[idx]})"
  end
  def GetIRDstType()
    "AnyBaseTypeToDataType(AnyBaseType::#{@res_type})"
  end

  
  def do_inline
    str = ""
    if @instruction
      raise if @intrinsic
      str += "auto inlined =
            GetGraph()->CreateInst#{@instruction.opcode}(#{GetIRDstType()}, callInst->GetPc());\n        "
      @args_type.each_with_index do |arg_type, idx|
        str += "inlined->SetInput(#{idx}, savedInputs_[#{idx}]);\n        "
      end
    else
      raise if @instruction
      str +=  "auto inlined =
            GetGraph()->CreateInstIntrinsic(#{GetIRDstType()}, callInst->GetPc(), RuntimeInterface::IntrinsicId::INTRINSIC_#{@intrinsic.enum_name});\n        "
      if @intrinsic.clear_flags.include? "require_state"
        str += "inlined->ReserveInputs(#{@args_type.size});
        inlined->AllocateInputTypes(GetGraph()->GetAllocator(), #{@args_type.size});\n        "
      else
        str += "inlined->ReserveInputs(#{@args_type.size + 1});
        inlined->AllocateInputTypes(GetGraph()->GetAllocator(), #{@args_type.size + 1});\n        "
      end
      @args_type.each_with_index do |arg_type, idx|
        str += "inlined->AppendInput(savedInputs_[#{idx}]);\n        "
        str += "inlined->AddInputType(#{IR.get_ir_type(@intrinsic.signature.args[idx])});\n        "
      end
      if !@intrinsic.clear_flags.include? "require_state"
        str += "inlined->AppendInput(callInst->GetSaveState());\n        "
        str += "inlined->AddInputType(DataType::NO_TYPE);\n        "
      end
    end

    str += "
        callInst->InsertAfter(inlined);
        auto castToAnyInst = GetGraph()->CreateInstCastValueToAnyType(callInst->GetPc(), AnyBaseType::#{@res_type}, inlined);
        callInst->ReplaceUsers(castToAnyInst);
        inlined->InsertAfter(castToAnyInst);
        callInst->GetBasicBlock()->RemoveInst(callInst);
    "
  end

  def intrinsic() @intrinsic end
  def instruction() @instruction end
  def args_type() @args_type end
  def res_type() @res_type end
end

class MethodDescr
  def initialize(ostr)
    raise "Not OpenStruct" if !ostr.kind_of?(OpenStruct)
    ostr.each_pair do |key, value|
      case key.to_s
      when "name"
        raise "`name` and `symb` shouldn't be used together" if @id != nil
        @id = JSString.new value
      when "symb"
        raise "`name` and `symb` shouldn't be used together" if @id != nil
        @id = JSSymbol.new value
      when "argc"
        @argc = value
      when "impl"
        raise "`impl` and `alias` shouldn't be used together" if @impl != nil
        @impl = CPPImpl.new value
      when "alias"
        raise "`impl` and `alias` shouldn't be used together" if @impl != nil
        @impl = Alias.new value
      when "ifdef"
        raise if !value.kind_of?(String)
        @ifdef_flag = value
      when "inline_info"
        begin
          @inline_info = convert_to_array_of_descr(InlinableDescr, value, @argc)
          @inline_info.delete_if {|info| (info.intrinsic == nil && info.instruction == nil)}
          @inline_info = nil if @inline_info.size == 0
        rescue RuntimeError => e
          raise "`#{@id.raw}`\n#{e.to_s}"
        end
      when "description"
      else
        raise "Unknown field: " + key.to_s
      end
    end
    # check created struct:
    raise "Missing `name` or `symb` fields" if @id == nil
    raise "Missing `argc` field" if (@argc == nil) && (@impl == nil)
    raise "`argc` should be of `Numeric`" if (@impl.kind_of?(CPPImpl) || @impl == nil) && !@argc.kind_of?(Numeric)
  end

  def gen_cpp_id
    raise "`impl` is redefined" if @impl != nil
    id.raw[0].upcase + id.raw[1..-1]
  end
  
  def gen_setfunction(namespace_name, obj_var_name)
    res = ""
    res += "#ifdef #{@ifdef_flag}\n" if @ifdef_flag
    if @id.kind_of?(JSString)
      res += "    SetFunction("
      res += "env, " if !@impl.kind_of?(Alias)
      res += "#{obj_var_name}, #{@id.to_cpp_value}, "
    elsif @id.kind_of?(JSSymbol)
      res += "    SetFunctionAtSymbol<JSSymbol::SymbolType::#{@id.raw.snakecase.upcase}>("
      res += "env, " if !@impl.kind_of?(Alias)
      res += "#{obj_var_name}, #{@id.to_cpp_value}, "
      res += "#{@id.to_cpp_literal}, " if !@impl.kind_of?(Alias)
    end
    if !@impl
      res += "#{namespace_name}::#{gen_cpp_id}, #{@argc});"
    elsif @impl.kind_of?(CPPImpl)
      res += "#{@impl.raw}, #{@argc});"
    elsif @impl.kind_of?(Alias)
      res += "#{@impl.gen_cpp_prop_getter});"
    else
      raise "Invalid `@impl` field in MethodDescr"
    end
    res += "\n#endif  // #{@ifdef_flag}" if @ifdef_flag
    res
  end
  
  def trivial_inline_overloads?
    raise if !@inline_info
    argc = @inline_info[0].args_type.size
    @inline_info.each do |info|
      if (info.args_type.size != argc) || (info.args_type.uniq.size > 1)
        return false
      end
    end
    return true
  end

  def get_integer_overload
    @inline_info.each do |info|
      if (info.args_type.uniq[0] == 'ECMASCRIPT_INT_TYPE')
        return info
      end
    end
    return nil
  end

  def get_double_overload
    @inline_info.each do |info|
      if (info.args_type.uniq[0] == 'ECMASCRIPT_DOUBLE_TYPE')
        return info
      end
    end
    return nil
  end
  
  def get_zero_overload
    raise if @inline_info.size != 1
    raise if @inline_info[0].args_type.size != 0
    return @inline_info[0]
  end

  def id() @id end
  def argc() @argc end
  def impl() @impl end
  def inline_info() @inline_info end
end

class GetDescr
  def initialize(ostr)
    raise "Not OpenStruct" if !ostr.kind_of?(OpenStruct)
    ostr.each_pair do |key, value|
      case key.to_s
      when "name"
        raise "`name` and `symb` shouldn't be used together" if @id != nil
        @id = JSString.new value
      when "symb"
        raise "`name` and `symb` shouldn't be used together" if @id != nil
        @id = JSSymbol.new value
      else
        raise "Unknown field: " + key.to_s
      end
    end
    # check created struct:
    raise "Missing `name` or `symb` fields" if @id == nil
  end

  def set_setter(setter)
    raise if @setter
    @setter = setter
  end

  def gen_creategetter(namespace_name, obj_var_name)
    res = "    JSHandle<JSTaggedValue> #{gen_cpp_id.snakecase} = CreateGetter(env, "
    if @impl
      res += "#{@impl.raw}, "
    else
      res += "#{namespace_name}::#{gen_cpp_id}, "
    end
    res += "#{@id.to_cpp_literal}, FunctionLength::ZERO);\n"
    res
  end

  def gen_setfunction(namespace_name, obj_var_name)
    res = ""
      res += "#ifdef #{@ifdef_flag}\n" if @ifdef_flag
      res += gen_creategetter(namespace_name, obj_var_name)
      res += @setter.gen_createsetter(namespace_name, obj_var_name) if @setter

      if @setter
        res += "    SetAccessor(#{obj_var_name}, #{@id.to_cpp_value}, #{gen_cpp_id.snakecase}, #{@setter.gen_cpp_id.snakecase});"
      else
        res += "    SetGetter(#{obj_var_name}, #{@id.to_cpp_value}, #{gen_cpp_id.snakecase});"
      end

      res += "\n#endif  // #{@ifdef_flag}" if @ifdef_flag
    res
  end

  def gen_cpp_id
    "Get" + id.raw[0].upcase + id.raw[1..-1]
  end

  def setter() @setter end
  def id() @id end
end

class SetDescr
  def initialize(ostr)
    raise "Not OpenStruct" if !ostr.kind_of?(OpenStruct)
    ostr.each_pair do |key, value|
      case key.to_s
      when "name"
        raise "`name` and `symb` shouldn't be used together" if @id != nil
        @id = JSString.new value
      when "symb"
        raise "`name` and `symb` shouldn't be used together" if @id != nil
        @id = JSSymbol.new value
      else
        raise "Unknown field: " + key.to_s
      end
    end
    # check created struct:
    raise "Missing `name` or `symb` fields" if @id == nil
  end

  def id() @id end
  def gen_cpp_id
    "Set" + id.raw[0].upcase + id.raw[1..-1]
  end

  def gen_createsetter(namespace_name, obj_var_name)
    res = "    JSHandle<JSTaggedValue> #{gen_cpp_id.snakecase} = CreateSetter(env, "
    if @impl
      res += "#{@impl.raw}, "
    else
      res += "#{namespace_name}::#{gen_cpp_id}, "
    end
    res += "#{@id.to_cpp_literal}, FunctionLength::ONE);\n"
    res
  end

  def ==(other)
    if other.kind_of?(GetDescr)
      other.id == @id
    else
      false
    end
  end
end

class PropertyDescr
  class AccessorDescr
    def initialize(ostr = nil)
      raise "Not OpenStruct" if ostr && !ostr.kind_of?(OpenStruct)
      @configurable = false
      @enumerable = false
      @writable = false
      if ostr
        ostr.each_pair do |key, value|
          raise "Accessor descriptors expected to be `true`/`false`" if !value.kind_of?(TrueClass) && !value.kind_of?(FalseClass)
          case key.to_s
          when "configurable"
            @configurable = value
          when "enumerable"
            @enumerable = value
          when "writable"
            @writable = value
          else
            raise "Unknown field: " + key.to_s
          end
        end
      end
    end

    def is_default?
      !@configurable && !@enumerable && !@writable
    end
  end
  
  def initialize(ostr)
    @accessor_descriptors = AccessorDescr.new
    raise "Not OpenStruct" if !ostr.kind_of?(OpenStruct)
    ostr.each_pair do |key, value|
      case key.to_s
      when "name"
        @id = JSString.new value
      when "impl"
        raise "`impl` and `str` shouldn't be used together" if @value != nil
        @value = CPPImpl.new value
      when "str"
        raise "`impl` and `str` shouldn't be used together" if @value != nil
        @value = JSString.new value
      when "accessor_descriptors"
        @accessor_descriptors = AccessorDescr.new value
      else
        raise "Unknown field: " + key.to_s
      end
    end

    def gen_propertydef
      res = ""
      if @value.kind_of?(CPPImpl)
        res += "inline auto GetProp#{@id.raw}()\n    {\n        return JSTaggedValue(#{@value.raw});  // NOLINT(readability-magic-numbers)\n    }"
        raise if !@accessor_descriptors.is_default?
      end
      res
    end

    # check created struct:
    raise "Missing `impl` or `str` fields" if @value == nil
    raise "Missing `name` field" if @id == nil
  end

  def id() @id end 
  def value() @value end 
  def accessor_descriptors() @accessor_descriptors end 
end

class AbstractOpDescr
  def initialize(abstract_op_decl)
    raise "Expected string" if !abstract_op_decl.kind_of? String
    @raw = abstract_op_decl
  end

  def decl
    @raw + ';'
  end
  
  def id
    @raw[/[^A-Za-z0-9_][A-Za-z0-9_]*/,0][1..-1]
  end
end


class Builtin
  def initialize(ostr, is_proto = false)
    @properties = []
    @getters = []
    @setters = []
    @methods = []
    @abstract_ops = []

    ostr.each_pair do |key, value|
      begin
        case key.to_s
        when "name"
          @name = value
        when "ctor"
          if ostr.name.to_s == "Function"
            @ctor = CtorDescr.new value, true
          else
            @ctor = CtorDescr.new value
          end
        when "properties"
          @properties = convert_to_array_of_descr(PropertyDescr, value)
        when "getters"
          @getters = convert_to_array_of_descr(GetDescr, value)
        when "setters"
          @setters = convert_to_array_of_descr(SetDescr, value)
        when "methods"
          @methods = convert_to_array_of_descr(MethodDescr, value)
        when "abstract_ops"
          @abstract_ops = convert_to_array_of_descr(AbstractOpDescr, value)
        when "prototype"
          @prototype = Builtin.new(value, true)
        else
          raise "Unknown field: " + key.to_s
        end
      rescue RuntimeError => e
        if is_proto
          raise "`#{key.to_s}`\n" + e.to_s
        else
          raise "Error in intiialization of\n`#{@name}`\n`#{key.to_s}`\n#{e.to_s}"
        end
      end
    end
    
    # Merge setters into getters; currently, every `setter` should have a corresponding `getter`
    getters.each do |getter|
      getter.set_setter(setters.delete(getter))
    end
    raise "Builtin space should have `name` field" if !@name && !is_proto
    raise "Prototype should not have `name` field" if @name && is_proto
    raise "Prototype should not have `prototype` field" if @prototype && is_proto
  end

  def inlinable_methods
    methods = []
    # currenlty, only `methods` can be inlined:
    @methods.each do |method|
      methods.append(method) if method.inline_info
    end
    methods
  end

  def cpp_ids
    ids = []
    if @ctor
      ids.append(@ctor.gen_cpp_id)
    end
    @methods.each do |method|
      ids.append(method.gen_cpp_id) if !method.impl
    end
    @getters.each do |getter|
      ids.append(getter.gen_cpp_id)
      ids.append(getter.setter.gen_cpp_id) if getter.setter
    end
    @setters.each do |setter|
      ids.append(setter.gen_cpp_id)
    end
    @abstract_ops.each do |abstract_op|
      ids.append(abstract_op.id)
    end
    ids
end

  def gen_cpp_decl(cpp_id)
    "JSTaggedValue #{cpp_id}(EcmaRuntimeCallInfo *argv);"
  end

  def cpp_functions_decls
      decls = []
      if @ctor
        decls.append(gen_cpp_decl(@ctor.gen_cpp_id))
      end
      @methods.each do |method|
        decls.append(gen_cpp_decl(method.gen_cpp_id)) if !method.impl
      end
      @getters.each do |getter|
        decls.append(gen_cpp_decl(getter.gen_cpp_id))
        decls.append(gen_cpp_decl(getter.setter.gen_cpp_id)) if getter.setter
      end
      @setters.each do |setter|
        decls.append(gen_cpp_decl(setter.gen_cpp_id))
      end
      @abstract_ops.each do |abstract_op|
        decls.append(abstract_op.decl)
      end
      decls
  end

  def name() @name end
  def ctor() @ctor end
  def getters() @getters end
  def setters() @setters end
  def methods() @methods end
  def properties() @properties end
  def prototype() @prototype end
end

class Builtins
  def self.wrap_data(data)
    ## Preload intrinsics as they may be used in `methods` construction:
    #data_intrinsics = YAML.load_file(ARGV[0])
    #data_intrinsics = JSON.parse(data_intrinsics.to_json, object_class: OpenStruct).freeze
    #Runtime.wrap_data(data_intrinsics)
    ## Preload intrinsics as they may be used in `methods` construction:
    #data_instructions = YAML.load_file(ARGV[1])
    #data_instructions = JSON.parse(data_instructions.to_json, object_class: OpenStruct).freeze
    #IR.wrap_data(data_instructions)
    
    @builtins = []
    data.builtins.each do |space|
      @builtins.append Builtin.new space
    end
    @builtins_internal = []
    data.builtins_internal.each do |name|
      @builtins_internal.append Builtin.new name
    end

  end
  def self.spaces() @builtins end
  def self.internal_spaces() @builtins_internal end
end

def Gen.on_require(data)
  Builtins.wrap_data(data)
end

def convert_to_array_of_descr(descr_class, array, opt_arg = nil)
  raise if !array.kind_of?(Array)
  res = []
  array.each do |elem|
    if (opt_arg != nil)
      res.append(descr_class.new(elem, opt_arg))
    else
      res.append(descr_class.new(elem))
    end
  end
  res
end
